Skip to main content

Stream API trong Java

Java Stream API, được giới thiệu từ Java 8, là một công cụ mạnh mẽ để xử lý tập hợp dữ liệu (collections) theo kiểu hàm (functional style). Stream API giúp xử lý dữ liệu một cách hiệu quả, linh hoạt và dễ đọc thông qua các thao tác xử lý (operations) được chia thành intermediate operations (xử lý trung gian) và terminal operations (xử lý cuối cùng). Bài viết dưới đây trình bày chi tiết về các implementation và các method của Stream API trong Java, kèm theo ví dụ thực tế cho từng nội dung.


1. Giới Thiệu Chung về Stream API

1.1. Định Nghĩa và Ưu Điểm

  • Stream là gì?
    Stream không phải là một cấu trúc dữ liệu lưu trữ, mà là một chuỗi các thao tác xử lý dữ liệu trừu tượng. Nó cho phép bạn tập trung vào "điều gì cần làm" thay vì "cách thức thực hiện".

  • Ưu điểm:

    • Xử lý theo kiểu khai báo: Code dễ hiểu và dễ bảo trì.
    • Lazy evaluation: Các intermediate operations chỉ được thực hiện khi có terminal operation, giúp tối ưu hiệu năng.
    • Parallel processing: Hỗ trợ xử lý song song dễ dàng chỉ bằng cách chuyển đổi stream sang parallel stream.

2. Các Cách Tạo Stream

2.1. Từ Collection

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
Stream<String> nameStream = names.stream(); // Stream tuần tự
Stream<String> parallelNameStream = names.parallelStream(); // Stream song song

2.2. Sử Dụng Stream.of()

Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);

2.3. Từ Mảng

String[] arr = {"Java", "C++", "Python"};
Stream<String> arrStream = Arrays.stream(arr);

2.4. Sử Dụng generate() và iterate()

  • generate(): Tạo stream bằng cách cung cấp một Supplier (không có đầu vào, trả về giá trị).
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
  • iterate(): Tạo stream từ một giá trị ban đầu, sau đó áp dụng một hàm để tính giá trị tiếp theo.
Stream<Integer> numbersIterated = Stream.iterate(0, n -> n + 2).limit(10);

3. Các Phương Thức của Stream API

Stream API phân thành hai loại thao tác chính:

  • Intermediate Operations (toán tử trung gian): Các thao tác chuyển đổi, lọc, ánh xạ, sắp xếp… chúng trả về một stream mới và được thực hiện theo lazy evaluation.
  • Terminal Operations (toán tử kết thúc): Các thao tác thu thập, tính toán kết quả, sau đó trả về kết quả cụ thể (không phải stream nữa) và kích hoạt quá trình xử lý của stream.

3.1. Các Intermediate Operations

a. filter(Predicate<T> predicate)

Lọc các phần tử thỏa mãn điều kiện.

List<Integer> numbersList = Arrays.asList(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbersList.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
// Kết quả: [2, 4, 6]

b. map(Function<T, R> mapper)

Chuyển đổi các phần tử trong stream theo hàm mapper cho trước.

List<String> lowerCaseNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toList());
// Nếu names = ["Alice", "Bob", "Charlie"] -> ["alice", "bob", "charlie"]

c. flatMap(Function<T, Stream<R>> mapper)

Chuyển đổi mỗi phần tử thành một stream, sau đó “làm phẳng” các stream con lại thành một stream duy nhất.

List<List<String>> listOfLists = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d"),
Arrays.asList("e", "f")
);

List<String> flatList = listOfLists.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// Kết quả: ["a", "b", "c", "d", "e", "f"]

d. distinct()

Loại bỏ các phần tử trùng lặp.

List<Integer> duplicates = Arrays.asList(1, 2, 2, 3, 3, 3, 4);
List<Integer> distinctList = duplicates.stream()
.distinct()
.collect(Collectors.toList());
// Kết quả: [1, 2, 3, 4]

e. sorted() và sorted(Comparator<T> comparator)

Sắp xếp các phần tử theo thứ tự tự nhiên hoặc theo thứ tự tùy chỉnh.

List<Integer> unsorted = Arrays.asList(5, 3, 1, 4, 2);
List<Integer> sortedList = unsorted.stream()
.sorted()
.collect(Collectors.toList());
// Kết quả: [1, 2, 3, 4, 5]

// Sắp xếp giảm dần:
List<Integer> sortedDesc = unsorted.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
// Kết quả: [5, 4, 3, 2, 1]

f. peek(Consumer<T> action)

Cho phép “nhìn” vào các phần tử của stream để thực hiện hành động mà không làm thay đổi chúng (thường dùng cho debug).

List<String> peekExample = names.stream()
.peek(name -> System.out.println("Đang xử lý: " + name))
.map(String::toUpperCase)
.collect(Collectors.toList());
// In ra tên trước khi chuyển thành chữ hoa.

g. limit(long maxSize)

Giới hạn số lượng phần tử trong stream.

List<Integer> limitedList = numbers.stream()
.limit(3)
.collect(Collectors.toList());
// Nếu numbers là [1,2,3,4,5] -> [1,2,3]

h. skip(long n)

Bỏ qua n phần tử đầu tiên của stream.

List<Integer> skippedList = numbers.stream()
.skip(2)
.collect(Collectors.toList());
// Nếu numbers là [1,2,3,4,5] -> [3,4,5]

3.2. Các Terminal Operations

a. forEach(Consumer<T> action)

Duyệt qua từng phần tử và thực hiện hành động, thường dùng để in ra hoặc ghi log.

names.stream()
.forEach(System.out::println);

b. collect(Collector<T, A, R> collector)

Thu thập các phần tử của stream thành một collection hoặc kiểu dữ liệu khác.

// Collect thành List
List<String> nameList = names.stream()
.collect(Collectors.toList());

// Collect thành Set
Set<String> nameSet = names.stream()
.collect(Collectors.toSet());

// Collect thành Map (ví dụ: key là tên, value là độ dài tên)
Map<String, Integer> nameMap = names.stream()
.collect(Collectors.toMap(name -> name, String::length));

c. reduce(BinaryOperator<T> accumulator)

Giảm stream thành một giá trị duy nhất thông qua hàm tích lũy.

List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> sum = nums.stream()
.reduce((a, b) -> a + b);
// Kết quả: Optional[15]

Có thể khởi tạo giá trị ban đầu cho reduce:

int sum2 = nums.stream()
.reduce(0, Integer::sum);
// Kết quả: 15

d. count()

Đếm số phần tử trong stream.

long count = names.stream().count();

e. anyMatch(Predicate<T> predicate), allMatch(Predicate<T> predicate), noneMatch(Predicate<T> predicate)

Kiểm tra điều kiện trên các phần tử của stream.

boolean anyStartsWithA = names.stream()
.anyMatch(name -> name.startsWith("A"));
// Trả về true nếu có ít nhất một tên bắt đầu bằng "A".

boolean allStartsWithA = names.stream()
.allMatch(name -> name.startsWith("A"));

boolean noneStartsWithZ = names.stream()
.noneMatch(name -> name.startsWith("Z"));

f. findFirst() và findAny()

Trả về phần tử đầu tiên hoặc bất kỳ phần tử nào của stream.

Optional<String> firstName = names.stream().findFirst();
Optional<String> anyName = names.stream().findAny();

g. min(Comparator<T> comparator) và max(Comparator<T> comparator)

Tìm phần tử nhỏ nhất hoặc lớn nhất theo comparator.

Optional<String> minName = names.stream()
.min(Comparator.naturalOrder());
Optional<String> maxName = names.stream()
.max(Comparator.naturalOrder());

4. Các Interface Chuyên Biệt: IntStream, LongStream, DoubleStream

Ngoài interface java Stream<T> cho các đối tượng, Java cung cấp các stream chuyên biệt cho các kiểu dữ liệu nguyên thủy nhằm tránh việc boxing/unboxing:

  • IntStream: Dành cho các giá trị kiểu int.
  • LongStream: Dành cho các giá trị kiểu long.
  • DoubleStream: Dành cho các giá trị kiểu double.

Ví dụ về IntStream:

IntStream.range(1, 10)           // Tạo stream với các số từ 1 đến 9
.filter(n -> n % 2 == 0)
.forEach(System.out::println);

5. Ví Dụ Thực Tế Ứng Dụng Stream API

5.1. Xử Lý Danh Sách Nhân Viên

Giả sử bạn có danh sách đối tượng Employee với các thuộc tính: id, tên, lương. Bạn muốn:

  • Lọc ra các nhân viên có lương cao hơn 5000.
  • Sắp xếp theo tên.
  • Trích xuất tên của họ.
class Employee {
private int id;
private String name;
private double salary;

// Constructor, getters, setters...
public Employee(int id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
}
public String getName() { return name; }
public double getSalary() { return salary; }
}

public class EmployeeStreamExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee(1, "Alice", 6000),
new Employee(2, "Bob", 4000),
new Employee(3, "Charlie", 7000),
new Employee(4, "David", 5000)
);

List<String> highSalaryEmployeeNames = employees.stream()
.filter(emp -> emp.getSalary() > 5000)
.sorted(Comparator.comparing(Employee::getName))
.map(Employee::getName)
.collect(Collectors.toList());

System.out.println("Nhân viên có lương > 5000: " + highSalaryEmployeeNames);
// Kết quả: [Alice, Charlie]
}
}

5.2. Phân Tích Số Liệu với Parallel Stream

Nếu bạn có tập dữ liệu lớn, việc sử dụng parallel stream có thể tăng tốc độ xử lý.

List<Integer> bigNumbers = IntStream.rangeClosed(1, 1_000_000)
.boxed()
.collect(Collectors.toList());

long countEven = bigNumbers.parallelStream()
.filter(n -> n % 2 == 0)
.count();

System.out.println("Số lượng số chẵn: " + countEven);

6. Tổng Kết

  • Stream API cung cấp một cách tiếp cận khai báo để xử lý tập hợp dữ liệu, hỗ trợ lazy evaluation, xử lý tuần tự và song song.
  • Các Intermediate Operations như filter, map, flatMap, sorted, distinct, peek, limit, skip cho phép biến đổi stream theo nhiều cách khác nhau.
  • Các Terminal Operations như forEach, collect, reduce, count, anyMatch, allMatch, findFirst, min, max kích hoạt chuỗi xử lý và trả về kết quả cuối cùng.
  • Các Stream chuyên biệt như IntStream, LongStreamDoubleStream giúp tối ưu hoá hiệu năng khi xử lý các kiểu dữ liệu nguyên thủy.
  • Các ví dụ thực tế cho thấy cách sử dụng Stream API để xử lý danh sách, lọc dữ liệu, ánh xạ và thu thập kết quả một cách ngắn gọn, dễ hiểu.

Nhờ vào Stream API, việc xử lý dữ liệu trong Java trở nên linh hoạt, dễ bảo trì và có thể tận dụng được sức mạnh của xử lý song song, phù hợp với các ứng dụng hiện đại yêu cầu hiệu năng cao và code sạch sẽ.